package com.redis.sdk;

import com.redis.sdk.cachecore.LockObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Redis cluster utils
 *
 * @description: redis集群操作类
 * @author: cyj
 * @date 2020 -10-28 14:59:26
 */
@Component
@Slf4j
public class RedisClusterUtils {

    /**
     * Redis template
     */
    @Autowired
    private RedisTemplate redisTemplate;

    private static final Long SUCCESS = 1L;

    /**
     * 续约lua脚本
     */
    private static final String RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";

    /**
     * 解锁lua脚本
     */
    private static final String UN_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";


    //==============================Redis set=============================================

    /**
     * Set *
     *
     * @param key   key
     * @param value value
     */
    public void set(Object key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * Set *
     *
     * @param key          key
     * @param value        value
     * @param milliSeconds milli seconds
     */
    public void set(Object key, Object value, long milliSeconds) {
        if (milliSeconds > 0) {
            redisTemplate.opsForValue().set(key, value, milliSeconds, TimeUnit.MILLISECONDS);
        } else {
            set(key, value);
        }
    }

    /**
     * Get object
     *
     * @param key key
     * @return the object
     * @description:
     */
    @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 1.0))
    public Object get(Object key) {
        return redisTemplate.opsForValue().get(key);
    }

    //==============================Redis hash=============================================

    /**
     * Hash exist boolean
     *
     * @param key    key
     * @param mapKey map key
     * @return the boolean
     * @description:
     */
    public boolean hashExist(Object key, Object mapKey) {
        return redisTemplate.opsForHash().hasKey(key, mapKey);
    }

    /**
     * Hash set *
     *
     * @param key       key
     * @param hashKey   hash key
     * @param hashValue hash value
     */
    public void hashSet(Object key, Object hashKey, Object hashValue) {
        redisTemplate.opsForHash().put(key, hashKey, hashValue);
    }

    /**
     * Hash get object
     *
     * @param key     key
     * @param hashKey hash key
     * @return the object
     * @description:
     */
    @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 1.0))
    public Object hashGet(Object key, Object hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    /**
     * Hash delete *
     *
     * @param key     key
     * @param hashKey hash key
     */
    public void hashDelete(Object key, Object hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    //==============================Redis com.base.common=============================================

    /**
     * Expire *
     *
     * @param key          key
     * @param milliSeconds milli seconds
     */
    public void expire(Object key, long milliSeconds) {
        if (milliSeconds > 0) {
            redisTemplate.expire(key, milliSeconds, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * Llt long
     *
     * @param key key
     * @return the long
     * @description:
     */
    public long llt(Object key) {
        return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
    }


    /**
     * Like set
     *
     * @param pattern
     * @return the set
     * @description: 于查找所有符合给定模式 pattern 的 key
     */
    public Set<Object> like(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * Recover object
     *
     * @param e e
     * @return the object
     * @description:
     */
    @Recover
    public Object recover(Exception e) {
        log.info("redis get retry failed!");
        return null;
    }

    /**
     * Delete *
     *
     * @param key key
     */
    public void delete(Object key) {
        redisTemplate.delete(key);
    }

    public void deletes(Set<Object> keys) {
        redisTemplate.delete(keys);
    }



    /**
     * Try lock boolean 尝试加锁 同 SET key value [EX seconds] [PX milliseconds] [NX|XX] 保证原子性
     *
     * @param key   key
     * @param value value
     * @return the boolean
     */
    public boolean tryLock(String key, Object value, long timeout, TimeUnit unit) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
    }

    /**
     * 阻塞至上锁成功
     * @return
     */
    public boolean lock(LockObject lockObject) {

        long waitTime = lockObject.getWaitTime();
        long yieldTime = (-1 == waitTime) ? -1 : System.currentTimeMillis() + waitTime * 1000;

        while (true) {
            boolean tryLock = tryLock(lockObject.getKey(), lockObject.getValue(), lockObject.getExpire(), lockObject.getTimeUnit());

            if (tryLock) {
                if (null != lockObject.getWatchDog()) {

                    // 放狗
                    lockObject.getWatchDog().schedule(() -> {
                        boolean expire = this.expire(lockObject);
                        log.info("{}, 续锁结果：{}", lockObject, expire);
                    }, lockObject.getExpire() / 3, lockObject.getTimeUnit());

                }
                return true;
            }

            if (yieldTime < System.currentTimeMillis() && -1 != yieldTime) {
                return false;
            }

            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                log.info("休眠失败");
            }

        }
    }

    /**
     * 续锁
     * @param lockObject
     * @return
     */
    public boolean expire(LockObject lockObject) {
        RedisScript<Long> redisScript = new DefaultRedisScript<>(RENEW_SCRIPT, Long.class);

        Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockObject.getKey()), lockObject.getValue());

        return SUCCESS.equals(result);
    }


    /**
     * 解锁
     *
     */
    public Boolean unlock(LockObject lockObject) {

        // 先锁狗
        if (null != lockObject.getWatchDog()) {
            lockObject.getWatchDog().shutdownNow();
        }

        RedisScript<Long> redisScript = new DefaultRedisScript<>(UN_LOCK_SCRIPT, Long.class);

        Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockObject.getKey()), lockObject.getValue());

        return SUCCESS.equals(result);
    }

    /**
     * unlink
     * @param key
     */
    public void unlink(Object key) {
        redisTemplate.unlink(key);
    }
}
